/*
 * Copyright 2002-2018 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.github.ramerf.wind.core.io;

import io.github.ramerf.wind.core.util.Asserts;
import io.github.ramerf.wind.core.util.PathUtils;
import java.io.*;
import java.net.URI;
import java.net.URL;
import java.nio.channels.*;
import java.nio.file.*;

/**
 * {@link Resource} implementation for {@code java.io.File} handles. Supports resolution as a {@code
 * File} and also as a {@code URL}. Implements the extended {@link WritableResource} interface.
 *
 * <p>Note: As of Spring Framework 5.0, this {@link Resource} implementation uses NIO.2 API for
 * read/write interactions. Nevertheless, in contrast to {@link PathResource}, it primarily manages
 * a {@code java.io.File} handle.
 *
 * @author Juergen Hoeller
 * @since 28.12.2003
 * @see PathResource
 * @see java.io.File
 * @see java.nio.file.Files
 */
public class FileSystemResource extends AbstractResource implements WritableResource {

  private final File file;

  private final String path;

  /**
   * Create a new {@code FileSystemResource} from a {@link File} handle.
   *
   * <p>Note: When building relative resources via {@link #createRelative}, the relative path will
   * apply <i>at the same directory level</i>: e.g. new File("C:/dir1"), relative path "dir2" ->
   * "C:/dir2"! If you prefer to have relative paths built underneath the given root directory, use
   * the {@link #FileSystemResource(String) constructor with a file path} to append a trailing slash
   * to the root path: "C:/dir1/", which indicates this directory as root for all relative paths.
   *
   * @param file a File handle
   */
  public FileSystemResource(File file) {
    Asserts.notNull(file, "File must not be null");
    this.file = file;
    this.path = PathUtils.cleanPath(file.getPath());
  }

  /**
   * Create a new {@code FileSystemResource} from a file path.
   *
   * <p>Note: When building relative resources via {@link #createRelative}, it makes a difference
   * whether the specified resource base path here ends with a slash or not. In the case of
   * "C:/dir1/", relative paths will be built underneath that root: e.g. relative path "dir2" ->
   * "C:/dir1/dir2". In the case of "C:/dir1", relative paths will apply at the same directory
   * level: relative path "dir2" -> "C:/dir2".
   *
   * @param path a file path
   */
  public FileSystemResource(String path) {
    Asserts.notNull(path, "Path must not be null");
    this.file = new File(path);
    this.path = PathUtils.cleanPath(path);
  }

  /** Return the file path for this resource. */
  public final String getPath() {
    return this.path;
  }

  /**
   * This implementation returns whether the underlying file exists.
   *
   * @see java.io.File#exists()
   */
  @Override
  public boolean exists() {
    return this.file.exists();
  }

  /**
   * This implementation checks whether the underlying file is marked as readable (and corresponds
   * to an actual file with content, not to a directory).
   *
   * @see java.io.File#canRead()
   * @see java.io.File#isDirectory()
   */
  @Override
  public boolean isReadable() {
    return (this.file.canRead() && !this.file.isDirectory());
  }

  /**
   * This implementation opens a NIO file stream for the underlying file.
   *
   * @see java.io.FileInputStream
   */
  @Override
  public InputStream getInputStream() throws IOException {
    try {
      return Files.newInputStream(this.file.toPath());
    } catch (NoSuchFileException ex) {
      throw new FileNotFoundException(ex.getMessage());
    }
  }

  /**
   * This implementation checks whether the underlying file is marked as writable (and corresponds
   * to an actual file with content, not to a directory).
   *
   * @see java.io.File#canWrite()
   * @see java.io.File#isDirectory()
   */
  @Override
  public boolean isWritable() {
    return (this.file.canWrite() && !this.file.isDirectory());
  }

  /**
   * This implementation opens a FileOutputStream for the underlying file.
   *
   * @see java.io.FileOutputStream
   */
  @Override
  public OutputStream getOutputStream() throws IOException {
    return Files.newOutputStream(this.file.toPath());
  }

  /**
   * This implementation returns a URL for the underlying file.
   *
   * @see java.io.File#toURI()
   */
  @Override
  public URL getURL() throws IOException {
    return this.file.toURI().toURL();
  }

  /**
   * This implementation returns a URI for the underlying file.
   *
   * @see java.io.File#toURI()
   */
  @Override
  public URI getURI() throws IOException {
    return this.file.toURI();
  }

  /** This implementation always indicates a file. */
  @Override
  public boolean isFile() {
    return true;
  }

  /** This implementation returns the underlying File reference. */
  @Override
  public File getFile() {
    return this.file;
  }

  /**
   * This implementation opens a FileChannel for the underlying file.
   *
   * @see java.nio.channels.FileChannel
   */
  @Override
  public ReadableByteChannel readableChannel() throws IOException {
    try {
      return FileChannel.open(this.file.toPath(), StandardOpenOption.READ);
    } catch (NoSuchFileException ex) {
      throw new FileNotFoundException(ex.getMessage());
    }
  }

  /**
   * This implementation opens a FileChannel for the underlying file.
   *
   * @see java.nio.channels.FileChannel
   */
  @Override
  public WritableByteChannel writableChannel() throws IOException {
    return FileChannel.open(this.file.toPath(), StandardOpenOption.WRITE);
  }

  /** This implementation returns the underlying File's length. */
  @Override
  public long contentLength() throws IOException {
    return this.file.length();
  }

  /**
   * This implementation creates a FileSystemResource, applying the given path relative to the path
   * of the underlying file of this resource descriptor.
   *
   * @see PathUtils#applyRelativePath(String, String)
   */
  @Override
  public Resource createRelative(String relativePath) {
    String pathToUse = PathUtils.applyRelativePath(this.path, relativePath);
    return new FileSystemResource(pathToUse);
  }

  /**
   * This implementation returns the name of the file.
   *
   * @see java.io.File#getName()
   */
  @Override
  public String getFilename() {
    return this.file.getName();
  }

  /**
   * This implementation returns a description that includes the absolute path of the file.
   *
   * @see java.io.File#getAbsolutePath()
   */
  @Override
  public String getDescription() {
    return "file [" + this.file.getAbsolutePath() + "]";
  }

  /** This implementation compares the underlying File references. */
  @Override
  public boolean equals(Object other) {
    return (this == other
        || (other instanceof FileSystemResource
            && this.path.equals(((FileSystemResource) other).path)));
  }

  /** This implementation returns the hash code of the underlying File reference. */
  @Override
  public int hashCode() {
    return this.path.hashCode();
  }
}
